package com.anlogic.sdk.utils;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

import org.apache.logging.log4j.Logger;
import org.eclipse.cdt.utils.spawner.ProcessFactory;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.IStreamListener;
import org.eclipse.debug.core.Launch;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamMonitor;
import org.eclipse.swt.graphics.Color;
import org.eclipse.ui.console.MessageConsole;
import org.eclipse.ui.console.MessageConsoleStream;

public class Launcher implements ICommandLauncher {
	private MessageConsoleStream stdoutStream;

	private MessageConsoleStream stderrStream;
	
	private MessageConsoleStream warningStream;

	private MessageConsoleStream launcherStream;

	private Process process = null;

	private IProcess dprocess = null;

	private String[] environment;

	private boolean printToConsole = true;
	
	private boolean redirectErrToOut = false;
	
	private boolean filterErrorMessage = false;

	private final StringBuffer output = new StringBuffer();
	
	private static Logger logger = LogUtils.getLogger(Launcher.class);

	public Launcher(MessageConsole console) {
		this.stdoutStream = console.newMessageStream();
		this.stderrStream = console.newMessageStream();
		this.warningStream = console.newMessageStream();
		this.launcherStream = console.newMessageStream();
		if (EnvUtils.isDarkTheme()) {
			this.stdoutStream.setColor(new Color(255, 255, 255));
			this.stderrStream.setColor(new Color(255, 0, 0));
			this.warningStream.setColor(new Color(255, 128, 0));
			this.launcherStream.setColor(new Color(0, 0, 0));
		} else {
			this.stdoutStream.setColor(new Color(99, 99, 99));
			this.stderrStream.setColor(new Color(255, 0, 0));
			this.warningStream.setColor(new Color(255, 128, 0));
			this.launcherStream.setColor(new Color(0, 0, 0));
		}
	}

	public int launch(String cmd) {
		launchWithParams(cmd, null, null);
		int status = waitForExit();
		if (status != 0) {
			logger.error("{} abnormal exit! Exit status is: {}", cmd, status);
		}
		return status;
	}

	public int launch(String cmd, File workDir, IStreamListener stdOutListener) {
		launchWithParams(cmd, workDir, stdOutListener);
		int status = waitForExit();
		if (status != 0) {
			logger.error("{} abnormal exit! Exit status is: {}", cmd, status);
		}
		return status;
	}

	public void asyncLaunch(String cmd, File workDir, IStreamListener stdOutListener) {
		launchWithParams(cmd, workDir, stdOutListener);
	}
	
	public void printStdOutMessage(String msg) {
		this.stdoutStream.println(msg);
	}

	public void printStdErrMessage(String msg) {
		this.stderrStream.println(msg);
	}

	private void launchWithParams(String cmd, File workDir, IStreamListener stdOutListener) {
		String[] cmdarr = getCmdArr(cmd);
		if (cmdarr == null) {
			logger.error("ProcLauncher can not be initialized, command is null!");
			return;
		}
		if (printToConsole)
			this.launcherStream.println(cmd);
		this.process = null;
		try {
			if (workDir != null) {
				this.process = ProcessFactory.getFactory().exec(cmdarr, this.environment, workDir);
			} else {
				this.process = ProcessFactory.getFactory().exec(cmdarr, this.environment);
			}
		} catch (IOException e) {
			logger.error("Unable to execute {}", cmd);
			throw new RuntimeException(e.getLocalizedMessage(), e);
		}
		Launch launch = new Launch(null, "run", null);
		this.dprocess = DebugPlugin.newProcess((ILaunch) launch, this.process, "Launcher");
		IStreamMonitor stdOut = this.dprocess.getStreamsProxy().getOutputStreamMonitor();
		IStreamMonitor stdErr = this.dprocess.getStreamsProxy().getErrorStreamMonitor();
		stdOut.addListener(new IStreamListener() {
			public void streamAppended(String text, IStreamMonitor monitor) {
				 synchronized (output) {
					 output.append(text);
				 }
				if (printToConsole) {
					Launcher.this.stdoutStream.print(text);
				}
			}
		});
		if (stdOutListener != null)
			stdOut.addListener(stdOutListener);
		stdErr.addListener(new IStreamListener() {
			public void streamAppended(String text, IStreamMonitor monitor) {
				if (redirectErrToOut) {
					 synchronized (output) {
						 output.append(text);
					 }
				}
				if (printToConsole) {
					if (filterErrorMessage) {
						if (text.startsWith("Error")) {
							Launcher.this.stderrStream.print(text);
						} else if (text.startsWith("Warn")) {
							Launcher.this.warningStream.print(text);
						} else {
							Launcher.this.stdoutStream.print(text);
						}
					} else {
						Launcher.this.stderrStream.print(text);
					}
				}
			}
		});
	}

	private String[] getCmdArr(String cmd) {
		if (cmd == null || cmd.isEmpty()) {
			return null;
		}
		String[] cmdarr = null;
		if (EnvUtils.isWindows()) {
			cmd = "cmd /c " + cmd;
		}
		String regex = " (?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"; // Skip double-quoted strings
		cmdarr = cmd.split(regex);
		return cmdarr;
	}
	
	private int waitForExit() {
		if (this.process == null)
			return EXEC_ERROR;
		int exitval = INCOMPLETE_ERROR;
		try {
			exitval = this.process.waitFor();
		} catch (InterruptedException e) {
			return exitval;
		}
		return exitval;
	}

	public boolean hasTerminated() {
		if (this.dprocess != null)
			return this.dprocess.isTerminated();
		return true;
	}

	public void terminate() {
		try {
	        if (dprocess != null && !dprocess.isTerminated()) {
	            dprocess.terminate();
	        }
		} catch (DebugException e) {
			logger.error("Terminate failed", e);
		} finally {
	        if (process != null && process.isAlive()) {
	            process.destroyForcibly();
	        }
	        dprocess = null;
	        process = null;
	    }
	}

	public void setEnvironment(String[] environment) {
		this.environment = Arrays.copyOf(environment, environment.length);
	}

	public void setPrintToConsole(boolean printToConsole) {
		this.printToConsole = printToConsole;
	}

	public void setRedirectErrToOut(boolean redirectErrToOut) {
		this.redirectErrToOut = redirectErrToOut;
	}
	
	public void setFilterErrorMessage(boolean filterErrorMessage) {
		this.filterErrorMessage = filterErrorMessage;
	}
	
	public String flush() {
	    String ret = "";
        for (int i = 0; i < 20; i++) {
            synchronized (output) {
                ret = output.toString();
            }
            if (!ret.isEmpty()) {
            	break;
            }
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
	    synchronized (output) {
	        output.setLength(0);
	    }
	    return ret;
	}
}
